﻿using Common.FrontEnd;
using FrontEnd.Util;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Text;
using Util.Props;

namespace FrontEnd.Window
{
    /// <summary>
    /// Slices up a Data object into a number of overlapping windows (usually referred to as "frames" in the speech world). In
    /// order to minimize the signal discontinuities at the boundaries of each frame, we multiply each frame with a raised
    /// cosine windowing function. Moreover, the system uses overlapping windows to capture information that may occur at the
    /// window boundaries. These events would not be well represented if the windows were juxtaposed. <p> The number of
    /// resulting windows depends on the {@link #PROP_WINDOW_SIZE_MS window size} and the {@link #PROP_WINDOW_SHIFT_MS window
    /// shift} (commonly known as frame shift in speech world). Figure 1 shows the relationship between the original data
    /// stream, the window size, the window shift, and the windows returned. <p> <img src="doc-files/framing.jpg">
    /// <br><b>Figure 1: Relationship between original data, window size, window shift, and the windows returned.</b> <p> The
    /// raised cosine windowing function will be applied to each such window. Since the {@link #getData()} method returns a
    /// window, and multiple windows are created for each Data object, this is a 1-to-many processor. Also note that the
    /// returned windows should have the same number of data points as the windowing function. <p> The applied windowing
    /// function, <i>W(n)</i>, of length <i>N</i> (the window size), is given by the following formula:
    /// <pre>
    /// W(n) = (1-a) - (a/// cos((2/// Math.PI/// n)/(N - 1)))
    /// </pre>
    /// where <b>a</b> is commonly known as the "alpha" value. This variable can be set by the user using the property
    /// defined by {@link #PROP_ALPHA}. Please follow the links to the see the constant field values. Some values of alpha
    /// receive special names, since they are used so often. A value of 0.46 for the alpha results in a window named Hamming
    /// window. A value of 0.5 results in the Hanning window. And a value of 0 results in the Rectangular window. The default
    /// for this system is the Hamming window, with alpha 0.46 !). Figure 2 below shows the Hamming window function (a =
    /// 0.46), using our default window size of 25.625 ms and assuming a sample rate of 16kHz, thus yielding 410 samples per
    /// window. <p> <img src="doc-files/hamming-window.gif"> <br><b>Figure 2: The Hamming window function.</b>
    ///
    /// @see Data
    /// </summary>
    public class RaisedCosineWindower : BaseDataProcessor
    {
        /** The property for window size in milliseconds. */
        [S4Double(defaultValue = 25.625)]
        public static String PROP_WINDOW_SIZE_MS = "windowSizeInMs";
        private float windowSizeInMs;

        /** The property for window shift in milliseconds, which has a default value of 10F. */
        [S4Double(defaultValue = 10.0)]
        public static String PROP_WINDOW_SHIFT_MS = "windowShiftInMs";
        private float windowShiftInMs;

        /** The property for the alpha value of the Window, which is the value for the RaisedCosineWindow. */
        [S4Double(defaultValue = 0.46)]
        public static String PROP_ALPHA = "alpha";
        private double alpha;


        // required to access the DataStartSignal-properties
        public static String WINDOW_SHIFT_SAMPLES = "windowSize";
        public static String WINDOW_SIZE_SAMPLES = "windowShift";

        private double[] cosineWindow; // the raised consine window
        private int windowShift; // the window size

        private List<IData> outputQueue; // cache for output windows
        private DoubleBuffer overflowBuffer; // cache for overlapped audio regions
        private long currentFirstSampleNumber;
        private int sampleRate;

        public RaisedCosineWindower( double alpha, float windowSizeInMs, float windowShiftInMs ) 
        {
            this.alpha = alpha;
            this.windowSizeInMs = windowSizeInMs;
            this.windowShiftInMs = windowShiftInMs;
        }

        public RaisedCosineWindower( ) 
        {

        }

        override
        public void newProperties(PropertySheet ps) 
        {
            base.newProperties(ps);

            alpha = ps.getDouble(PROP_ALPHA);
            windowSizeInMs = ps.getFloat(PROP_WINDOW_SIZE_MS);
            windowShiftInMs = ps.getFloat(PROP_WINDOW_SHIFT_MS);
        }

        /// <summary>
        /// @see Sphincs.frontend.DataProcessor#initialize(Sphincs.frontend.CommonConfig)
        /// </summary>
        override public void initialize() 
        {
            base.initialize();

            // createWindow();
            outputQueue = new List<IData>();
        }

        /// <summary>
        /// Creates the Hamming Window.
        /// </summary>
        /// <param name="sampleRate"></param>
        private void createWindow(int sampleRate) 
        {
            if (cosineWindow != null && sampleRate == this.sampleRate) 
            {
                return;
            }

            this.sampleRate = sampleRate;

            int windowSize = DataUtil.getSamplesPerWindow(sampleRate, windowSizeInMs);
            cosineWindow = new double[windowSize];

            windowShift = DataUtil.getSamplesPerShift(sampleRate, windowShiftInMs);

            if (cosineWindow.Length > 1) 
            {
                double oneMinusAlpha = (1 - alpha);
                for (int i = 0; i < cosineWindow.Length; i++) {
                    cosineWindow[i] = oneMinusAlpha -
                            alpha* Math.Cos(2* Math.PI* i / (cosineWindow.Length - 1.0));
                }
            }

            overflowBuffer = new DoubleBuffer(windowSize);
        }

        /// <summary>
        /// Returns the next Data object, which is usually a window of the input Data, with the windowing function applied to
        /// it.
        /// </summary>
        /// <returns>the next available Data object, returns null if no Data object is available</returns>
        override public IData getData()
        {
            if (outputQueue.Count==0) 
            {
                IData input = getPredecessor().getData();

                getTimer().start();

                if (input != null) 
                {
                    if (input is DoubleData) 
                    {
                        DoubleData data = (DoubleData) input;
                        // System.err.print("to windower: ");
                        // System.err.println(Arrays.toString(data.getValues()));
                        if (currentFirstSampleNumber == -1) {
                            currentFirstSampleNumber = data.getFirstSampleNumber();
                        }

                        // should not be necessary if all DataProcessor would forward Signals. Unfortunately this
                        // is currently not the case.
                        createWindow(data.getSampleRate());

                        // process the Data, and output the windows
                        process(data);
                    } 
                    else 
                    {
                        if (input is DataStartSignal) 
                        {
                            DataStartSignal startSignal = (DataStartSignal) input;

                            createWindow(startSignal.SampleRate);
			
                	        // attach the frame-length and the shift-length to the start-signal to allow
			                // detection of incorrect frontend settings
                            Dictionary<String, Object> props = startSignal.getProps();
                            props.Add(WINDOW_SHIFT_SAMPLES, windowShift);
                            props.Add(WINDOW_SIZE_SAMPLES, cosineWindow.Length);

                            // reset the current first sample number
                            currentFirstSampleNumber = -1;
                        } 
                        else if (input is SpeechStartSignal) 
                        {
                            // reset the current first sample number
                            currentFirstSampleNumber = -1;		    
                        } 
                        else if (input is DataEndSignal || input is SpeechEndSignal) 
                        {
                            // end of utterance handling
                            processUtteranceEnd();
                        }
		    
                        outputQueue.Add(input);
                    }
                }

                getTimer().stop();
            }

            if (outputQueue.Count!=0) 
            {
                IData output = outputQueue[0];
                outputQueue.RemoveAt(0);
                if (output is DoubleData) 
                {
                    Trace.Assert(((DoubleData) output).Values.Length == cosineWindow.Length);
                }
                return output;
            } 
            else 
            {
                return null;
            }
        }


        /// <summary>
        /// Applies the Windowing to the given Data. The resulting windows are cached in the outputQueue.
        /// </summary>
        /// <param name="input">the input Data object</param>
        private void process(DoubleData input)
        {
            double[] _in = input.Values;
            int length = overflowBuffer.getOccupancy() + _in.Length;

            List<DoubleData> dataList = new List<DoubleData>();
            dataList.Add(input);

            IData utteranceEnd = null;

            // read in more Data if we have under one window's length of data
            while (length < cosineWindow.Length) 
            {
                IData next = getPredecessor().getData();
                if (next is DoubleData) 
                {
                    dataList.Add((DoubleData) next);
                    length += ((DoubleData) next).Values.Length;
                } 
                else 
                {
                    if (next is DataEndSignal || next is SpeechEndSignal) 
                    {
                        utteranceEnd = next;
                        break;
                    }

                    outputQueue.Add(next);
                }
            }

            double[] allSamples = _in;

            // prepend overflow samples
            if (length != _in.Length) 
            {

                allSamples = new double[length];

                // copy overflow samples to allSamples buffer
                Array.Copy(overflowBuffer.getBuffer(), 0, allSamples, 0, overflowBuffer.getOccupancy());
                int start = overflowBuffer.getOccupancy();

                // copy input samples to allSamples buffer
                foreach (DoubleData aDataList in dataList) 
                {
                    double[] samples = aDataList.Values;
                    Array.Copy(samples, 0, allSamples, start, samples.Length);
                    start += samples.Length;
                }
            }

            // apply Hamming window
            int residual = applyRaisedCosineWindow(allSamples, length);

            // save elements that also belong to the next window
            overflowBuffer.reset();
            if (length - residual > 0) 
            {
                overflowBuffer.append(allSamples, residual, length - residual);
            }
            if (utteranceEnd != null) 
            {
                // end of utterance handling
                processUtteranceEnd();
                outputQueue.Add(utteranceEnd);
            }
        }


        /**
        /// What happens when an DataEndSignal is received. Basically pads up to a window of the overflow buffer with zeros,
        /// and then apply the Hamming window to it. Checks if buffer has data.
         */
        private void processUtteranceEnd() 
        {
	        if (overflowBuffer.getOccupancy() > 0) 
            {
    	        overflowBuffer.padWindow(cosineWindow.Length);
	            applyRaisedCosineWindow(overflowBuffer.getBuffer(), cosineWindow.Length);
    	        overflowBuffer.reset();
            }
        }


        /// <summary>
        /// Applies the Hamming window to the given double array. The windows are added to the output queue. Returns the
        /// index of the first array element of next window that is not produced because of insufficient data.
        /// </summary>
        /// <param name="_in">the audio data to apply window and the Hamming window</param>
        /// <param name="length">the number of elements in the array to apply the RaisedCosineWindow</param>
        /// <returns>the index of the first array element of the next window</returns>
        private int applyRaisedCosineWindow(double[] _in, int length) 
        {
            int windowCount = 0;

            // if no windows can be created but there is some data,
            // pad it with zeros
            if (length < cosineWindow.Length) 
            {
                double[] padded = new double[cosineWindow.Length];
                Array.Copy(_in, 0, padded, 0, length);
                _in = padded;
                windowCount = 1;
            } 
            else 
            {
                windowCount = getWindowCount(length, cosineWindow.Length, windowShift);
            }

            // create all the windows at once, not individually, saves time
            double[][] windows = new double[windowCount][/**/];
            for(int element = 0; element < windowCount; element++)
                windows[element] = new double[cosineWindow.Length];

            int windowStart = 0;

            for (int i = 0; i < windowCount; windowStart += windowShift, i++) {

                double[] myWindow = windows[i];

                // apply the Hamming Window function to the window of data
                for (int w = 0, s = windowStart; w < myWindow.Length; s++, w++) {
                    myWindow[w] = _in[s]* cosineWindow[w];
                }

                // add the frame to the output queue
                outputQueue.Add(new DoubleData
                        (myWindow, sampleRate,
                                currentFirstSampleNumber));
                currentFirstSampleNumber += windowShift;
            }

            return windowStart;
        }


        /**
        /// Returns the number of windows in the given array, given the windowSize and windowShift.
         *
        /// @param arraySize   the size of the array
        /// @param windowSize  the window size
        /// @param windowShift the window shift
        /// @return the number of windows
         */
        private static int getWindowCount(int arraySize, int windowSize,int windowShift) 
        {
            if (arraySize < windowSize) 
            {
                return 0;
            }
            int windowCount = 1;
            for (int windowEnd = windowSize;
                    windowEnd + windowShift <= arraySize;
                    windowEnd += windowShift) 
            {
                windowCount++;
            }
            return windowCount;

        }


        /**
        /// Returns the shift size used to window the incoming speech signal. This value might be used by other components to
        /// determine the time resolution of feature vectors.
        /// @return the shift of the window
         */
        public float getWindowShiftInMs() 
        {
            if (windowShiftInMs == 0)
                throw new SystemException(this + " was not initialized yet!");

            return windowShiftInMs;
        }


        public int getSampleRate() 
        {
            return sampleRate;
        }


        /**
        /// Rounds a given sample-number to the number of samples will be processed by this instance including the padding
        /// samples at the end..
        /// @param samples
         */
        public long roundToFrames(long samples) 
        {
            int windowSize = DataUtil.getSamplesPerWindow(sampleRate, windowSizeInMs);
            int windowShift = DataUtil.getSamplesPerShift(sampleRate, windowShiftInMs);

            long mxNumShifts = samples / windowShift;

            for (int i = (int) mxNumShifts; ; i--) {
                long remainingSamples = samples - windowShift* i;

                if (remainingSamples > windowSize)
                    return windowShift* (i + 1) + windowSize;
            }
        }
    }


    public class DoubleBuffer 
    {

        private double[] buffer;
        private int occupancy;


        /** Constructs a DoubleBuffer of the given size.
        /// @param size*/
        public DoubleBuffer(int size) {
            buffer = new double[size];
            occupancy = 0;
        }


        /**
        /// Returns the number of elements in this DoubleBuffer.
         *
        /// @return the number of elements in this DoubleBuffer.
         */
        public int getOccupancy() {
            return occupancy;
        }


        /**
        /// Returns the underlying double array used to store the data.
         *
        /// @return the underlying double array
         */
        public double[] getBuffer() {
            return buffer;
        }


        /**
        /// Appends all the elements in the given array to this DoubleBuffer.
         *
        /// @param src the array to copy from
        /// @return the resulting number of elements in this DoubleBuffer.
         */
        public int appendAll(double[] src) {
            return append(src, 0, src.Length);
        }


        /**
        /// Appends the specified elements in the given array to this DoubleBuffer.
         *
        /// @param src    the array to copy from
        /// @param srcPos where in the source array to start from
        /// @param length the number of elements to copy
        /// @return the resulting number of elements in this DoubleBuffer
         */
        public int append(double[] src, int srcPos, int length) {
            if (occupancy + length > buffer.Length) {
                throw new Exception("RaisedCosineWindower: " +
                        "overflow-buffer: attempting to fill " +
                        "buffer beyond its capacity.");
            }
            Array.Copy(src, srcPos, buffer, occupancy, length);
            occupancy += length;
            return occupancy;
        }


        /**
        /// If there are less than windowSize elements in this DoubleBuffer, pad the up to windowSize elements with zero.
         *
        /// @param windowSize the window size
         */
        public void padWindow(int windowSize) 
        {
            if (occupancy < windowSize) 
            {
                for(int i= occupancy;i<windowSize;i++)
                    buffer[i]=0;
            }
        }


        /** Sets the number of elements in this DoubleBuffer to zero, without actually remove the elements. */
        public void reset() {
            occupancy = 0;
        }
    }
}
